package com.google.code.mochaccino.framework.search;
/*
 * Copyright 2012 Claude Houle claude.houle@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.google.common.base.Preconditions;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.ConcurrentMergeScheduler;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.KeepOnlyLastCommitDeletionPolicy;
import org.apache.lucene.index.TieredMergePolicy;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.NativeFSLockFactory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;

/**
 * The Search Index is a simple abstraction over Lucene to provide easier
 * manipulation
 */
public final class SearchIndex {

	/**
	 * The Query Analyzer for the content of this search index. It is recommended
	 * that the analyzer is the same for all the lifetime of this index. If you
	 * change the analyzer, you will need to re-index the whole data to have an accurate
	 * index.
	 */
	private Analyzer analyzer = null;

	/** The Directory for this index. */
	private Directory directory = null;

	/** The Index Writer Configuration for this Search Index. */
	private IndexWriterConfig writerConfig = null;

	/** Searcher Lock */
	private ReentrantReadWriteLock searcherLock = new ReentrantReadWriteLock( false );

	/** The Default Field */
	private String defaultField = null;

	/** The Thread Local Query Parser */
	private ThreadLocal<QueryParser> queryParser = new ThreadLocal<QueryParser>();

	/** The Version of the Index */
	private Version version = Version.LUCENE_35;

	/** The Index Searcher */
	protected IndexSearcher searcher = null;

	/** The Index Writer */
	protected IndexWriter writer = null;

	/** Constructor */
	public SearchIndex( File dir, String field, Analyzer analyzer ) throws IOException {
		this( dir, field, analyzer, OpenMode.CREATE_OR_APPEND );
	}

	/** Constructor */
	public SearchIndex( File dir, String field, Analyzer analyzer, OpenMode mode ) throws IOException {
		this.analyzer = Preconditions.checkNotNull( analyzer );
		this.defaultField = Preconditions.checkNotNull( field );
		this.directory = FSDirectory.open( Preconditions.checkNotNull( dir ), new NativeFSLockFactory() );
		createWriterConfig( mode );
		initIndex();
	}

	/** Constructor */
	public SearchIndex( String field, Analyzer analyzer ) throws IOException {
		this.directory = new RAMDirectory();
		this.defaultField = Preconditions.checkNotNull( field );
		this.analyzer = Preconditions.checkNotNull( analyzer );
		createWriterConfig( OpenMode.CREATE_OR_APPEND );
		initIndex();
	}

	/** Create Writer Configuration */
	private void createWriterConfig( OpenMode mode ) throws IOException {
		writerConfig = new IndexWriterConfig( version, analyzer );
		writerConfig.setOpenMode( Preconditions.checkNotNull( mode ) );
		writerConfig.setMaxBufferedDocs( IndexWriterConfig.DEFAULT_MAX_BUFFERED_DOCS );
		writerConfig.setMaxThreadStates( IndexWriterConfig.DEFAULT_MAX_THREAD_STATES );
		writerConfig.setMaxBufferedDeleteTerms( IndexWriterConfig.DEFAULT_MAX_BUFFERED_DELETE_TERMS );
		writerConfig.setMergeScheduler( new ConcurrentMergeScheduler() );
		writerConfig.setRAMBufferSizeMB( IndexWriterConfig.DEFAULT_RAM_BUFFER_SIZE_MB );
		writerConfig.setReaderPooling( true );
		writerConfig.setTermIndexInterval( IndexWriterConfig.DEFAULT_TERM_INDEX_INTERVAL );
		writerConfig.setWriteLockTimeout( TimeUnit.SECONDS.toMillis( 1 ) );
		writerConfig.setIndexDeletionPolicy( new KeepOnlyLastCommitDeletionPolicy() );
		writerConfig.setMergePolicy( new TieredMergePolicy() );
	}

	private void initIndex() throws IOException {
		this.writer = new IndexWriter( directory, writerConfig );
		this.writer.commit();
		this.searcher = new IndexSearcher( IndexReader.open( directory ) );
	}

	/** Close the Index */
	public void close() throws IOException {
		WriteLock lock = searcherLock.writeLock();
		lock.lock();
		try {
			if ( searcher != null ) {
				this.searcher.close();
				this.searcher = null;
			}

			if ( this.writer != null ) {
				this.writer.close();
				this.writer = null;
			}

			if ( directory != null ) {
				this.directory.close();
				this.directory = null;
			}
		} finally {
			lock.unlock();
		}
	}

	/** Get Document */
	protected Document getDocument( int docId ) throws IOException {
		ReadLock lock = searcherLock.readLock();
		lock.lock();
		try {
			return searcher.doc( docId );
		} finally {
			lock.unlock();
		}
	}

	/** Return an Index Updater */
	public SearchIndexUpdater getUpdater() {
		return new SearchIndexUpdater( writer );
	}

	/** Is Reopen */
	public boolean reopen() throws IOException {
		if ( !isCurrent() ) {
			WriteLock lock = searcherLock.writeLock();
			lock.lock();
			try {
				IndexReader reader = this.searcher.getIndexReader();
				searcher.close();
				searcher = new IndexSearcher( IndexReader.open( directory ) );
				reader.close();
				return true;
			} finally {
				lock.unlock();
			}
		}
		return false;
	}

	/** Is Current */
	public boolean isCurrent() throws IOException {
		ReadLock lock = searcherLock.readLock();
		lock.lock();
		IndexReader reader = this.searcher.getIndexReader();
		try {
			return reader.isCurrent();
		} finally {
			lock.unlock();
		}
	}

	/** Search */
	public Iterator<Document> search( Query query, Filter filter, int maxResultCount ) throws IOException {
		ReadLock lock = searcherLock.readLock();

		lock.lock();
		try {
			return new DocumentIterator( searcher.search( query, filter, maxResultCount ) );
		} finally {
			lock.unlock();
		}
	}

	/** Search */
	public Iterator<Document> search( Query query, Filter filter, int maxResultCount, Sort sort ) throws IOException {
		ReadLock lock = searcherLock.readLock();
		lock.lock();
		try {
			return new DocumentIterator( searcher.search( query, filter, maxResultCount, Preconditions.checkNotNull( sort ) ) );
		} finally {
			lock.unlock();
		}
	}

	/** Search */
	public Iterator<Document> search( Query queryString, int maxResultCount, Sort sort ) throws IOException {
		return search( queryString, null, maxResultCount, Preconditions.checkNotNull( sort ) );
	}

	/** Search */
	public Iterator<Document> search( Query queryString, int maxResultCount ) throws IOException {
		return search( queryString, null, maxResultCount );
	}

	/** Search */
	public Iterator<Document> search( String queryString, Filter filter, int maxResultCount ) throws ParseException, IOException {
		Query query = parseQuery( queryString );
		return search( query, filter, maxResultCount );
	}

	/** Search */
	public Iterator<Document> search( String queryString, int maxResultCount ) throws ParseException, IOException {
		Query query = parseQuery( queryString );
		return search( query, null, maxResultCount );
	}

	/** Search */
	public Iterator<Document> search( String queryString, Filter filter, int maxResultCount, Sort sort ) throws ParseException, IOException {
		Query query = parseQuery( queryString );
		return search( query, filter, maxResultCount, Preconditions.checkNotNull( sort ) );
	}

	/** Search */
	public Iterator<Document> search( String queryString, int maxResultCount, Sort sort ) throws ParseException, IOException {
		Query query = parseQuery( queryString );
		return search( query, null, maxResultCount, Preconditions.checkNotNull( sort ) );
	}

	/** Parse Query */
	private Query parseQuery( String queryString ) throws ParseException, IOException {
		QueryParser parser = queryParser.get();
		if ( parser == null ) {
			parser = new QueryParser( version, defaultField, analyzer );
			queryParser.set( parser );
		}

		ReadLock lock = searcherLock.readLock();
		lock.lock();
		try {
			Query query = parser.parse( queryString );
			query = searcher.rewrite( query );
			return query;
		} finally {
			lock.unlock();
		}
	}

	protected class DocumentIterator implements Iterator<Document> {
		private AtomicInteger idx          = new AtomicInteger();
		private TopDocs       topDocuments = null;

		public DocumentIterator( TopDocs docs ) {
			this.topDocuments = docs;
		}

		@Override
		public boolean hasNext() {
			return idx.get() < topDocuments.scoreDocs.length;
		}

		@Override
		public Document next() {
			int docId = topDocuments.scoreDocs[idx.getAndIncrement()].doc;
			try {
				return getDocument( docId );
			} catch ( CorruptIndexException e ) {
				throw new RuntimeException( e );
			} catch ( IOException e ) {
				throw new RuntimeException( e );
			}
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}
	}
}
